<?php
class MiscHelper {
	
	/**
	 * Первая заглавная буква
	 * @param unknown $string
	 * @return string
	 */
	public static function firstToUpper($string) {
		$string = ltrim($string);
		if (!function_exists('mb_ucfirst') && function_exists('mb_substr')) {
			$string = mb_strtoupper(mb_substr($string, 0, 1)) . mb_substr($string, 1);
		}
		else 
			$string = mb_ucfirst($string);
		
		return $string;
	}
	
	/**
	 * Load the content from the $url, over proxy (if present)
	 * @param mixed   $url          The URI path. string или array.
	 *
	 *   вместо Url может быть передан массив следующей структуры:
	 *   TODO: в будущем нужно сделать данный вызов единственно верным!
	 *   array(
	 *       'url'    => 'Url',
	 *       'proxy'  => 'прокси',
	 *       'ip'     => 'Ip адрес',
	 *       'content'       =>'Тело запроса'
	 *       'cookie_path'   =>'путь к файлу cookie'
	 *       'logfile'       => 'использовать или нет логирование'
	 *       'followlocation'=>'делать редирект или нет'
	 *       'timeout'       => время ожидания ответа для curl
	 *       'callback'      => Фуннкция обработки результата
	 *                         array('someClassObject', 'someClassMethod')
	 *       'mimefilter'    =>array('название mime') Отсекает контент не совпадающий  с заданным
	 *   )
	 *
	 * @param string  $proxy        The URI path to proxy.Optional.
	 * @param string  $ip           Valid ip address
	 * @param string  $content      Content for sending to $url
	 * @param handler $logFile      Opened file handler
	 * @param string  $cookie_path  Real path to file using as cookie storage
	 * @return mixed string или array
	 */
	
	/**
	 * Загружает данные по списку url с указанными опциями
	 * Все поданные URL проходят принудительное преобразование idn_to_ascii
	 * @param array $url список url
	 * Данный списко может быть в влед форматах форматов:
	 *   тип1)
	 *   string Строка url. Будет выполнен запрос на указанный  url. В данном случае $opt будет проинорирован.
	 *
	 *   тип2)
	 *   простой список вида array('url1', 'url2'..'urlN')
	 *
	 *   тип3)
	 *   список с опциями для  каждого url, подразумевается использование опций сооветствующих опциям cUrl
	 *    например:
	 *      array(
	 *          CURLOPT_URL=>'http://site.ru'
	 *          CURLOPT_POSTFIELDS=>'my post data'
	 *          ....
	 *          Любые ключи разрешенные для использова с cUrl
	 *          )
	 *
	 *   Кроме того разрешены  сокрашенные варианты  для некоторых ключей:
	 *   array(
	 *      array(
	 *       'url'=>'http://site.ru', в данном  случае этот ключ будет использован с CURLOPT_URL при выполнении запроса.
	 *                               вместо ключа 'url' можно использовать ключ CURLOPT_URL в явном виде. оба использования эквивалентны
	 *
	 *       'callback'=> array(), Фуннкция обратного вызова для обработки результата сразу по получению, не дожидаясь
	 *                           окончания  сбора  всех url.
	 *                           Задается массивом подходящим  для использования  с  call_user_func_array как массив:
	 *                           array('someClassObject', 'someClassMethod', 'arg5',...,'argN')
	 *                           При вызове в функцию передаются следующие праметры:
	 *
	 *                           someClassObject->someClassMethod($u, (string)$content, $info, $ch, $arg5,...,$argN);
	 *                           где $u         Ключ массива результатов
	 *                               $content   Контент с указанного url
	 *                               $info      Код ответа по спец. multi_curl
	 *                               $ch        Объект cURL
	 *                               $arg5 - N  Дополнительные параметры
	 *
	 *       'proxy'         => прокси', Адресс прокси через который нужно отправить запрос,  CURLOPT_PROXY
	 *       'ip'            => Ip адрес', Ip через который будет отправлен запрос,           CURLOPT_INTERFACE
	 *       'content'       => Тело запроса                                                  CURLOPT_POSTFIELDS
	 *       'cookie_path'   => путь к файлу cookie                                           CURLOPT_COOKIEFILE,
	 *                                                                                        CURLOPT_COOKIEJAR
	 *       'followlocation'=> делать редирект или нет                                       CURLOPT_FOLLOWLOCATIO
	 *       'timeout'       => время ожидания ответа для curl                                CURLOPT_TIMEOUT
	 *       'user_agent'    => описание от какого браузера  делать запрос                    CURLOPT_USERAGENT
	 *       'headers'       => Заголовки для curl                                            CURLOPT_HTTPHEADER
	 *
	 *                                 ---без соответствий---
	 *
	 *       'mimefilter'    =>array('название mime') Отсекает контент не совпадающий с заданным mime
	 *       'logfile'       => 'использовать или нет логирование',
	 *       'cached'        => будет ли сохраняться в кэш результат выполнения запроса
	 *      )
	 *      ....
	 *      другие  url заданные в этом  же  формате
	 *   )
	 * @param array $opt Любые ключи разрешенные для использова с cUrl. Задание опций на все обрабатываемые url разом.
	 * Для $url тип3 применяется  правило,
	 * опции заданные  для  конкретного url имеют больший вес. Те  для запроса будут использованны персональные для url
	 * опции  а не глобальные  заданные через $opt
	 * в $opt
	 * запрещены ключи:
	 *  'url' , CURLOPT_URL  при обнаружении таких ключей выкидывается исключение.
	 * разрешены ключи:
	 *  все остальные допустимые  для тип3)
	 *
	 * @param integer $countTasks количество одновременно выполняемых запросов, используется с тип2 и тип3
	 * @return mixed Данные полученные с указанных url по указанным  опциям.
	 */
	public static function getContent($url = null, $opt = null, $countTasks = 100) {
		// проверка начальных данных на валидность
		if(is_null($url))
			throw new CException('MiscHelper::getContent(): Попытка получить контент без указания откуда $url.');
	
		if(is_array($opt) && (isset($opt['url']) || isset($opt[CURLOPT_URL])))
			throw new CException('Попытка задать запрещенные опции в $opt');
	
		$type=0;// по какому  типу  запросов  работаем
		//Для исключения  проверок на массив, зададим  $opt
		if(is_null($opt)) $opt=array();
	
		//настройка user agent
		$userAgent = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:15.0) Gecko/20100101 Firefox/15.0";
		if(!isset($opt['user_agent']) || !isset($opt[CURLOPT_USERAGENT]))
			$opt['user_agent']=$userAgent;
		// столкнулись с ситуацией, когда соединения стали кэшироваться, т.е. опция CURLOPT_INTERFACE
		// попросту игнорировалась, запрос уходил с IP кэшированного соединения, а не с тем
		// который задавался
		$opt[CURLOPT_FORBID_REUSE] = 1;
		$opt[CURLOPT_FRESH_CONNECT] = 1;
	
		//обрабатка URL тип 1)
		if(!is_array($url)){
			$type=1;//Просто формальность, чтобы  было одно типно
			$ch=self::getCurl(array(CURLOPT_URL=>$url) + $opt);
	
			$out = curl_exec($ch);
			//пишем лог если надо
			if ( isset($opt['logfile']) && is_resource($opt['logfile']) && curl_errno($ch) >0 ) fwrite($opt['logfile'], curl_error($ch)."\n\n");
	
			if(isset($opt['callback']) && is_array($opt['callback']) && count($opt['callback']) >= 2) {
				// если передана callback-функция, вызываем её для обработки результата
				// пред последний параметр результат выполнения CURL
				// последний параметр сам CURL
				$callback = array($opt['callback'][0], $opt['callback'][1]);
				array_splice($opt['callback'], 0, 2); // если функция принимает параметры по ссылке, то их и подавать нужно по ссылке
				$out = call_user_func_array(
						$callback,
						array_merge(array(0, $out, curl_errno($ch), $ch), $opt['callback'])
				);
			}
			curl_close($ch);
	
			return $out;
		}
	
		//обработка url тип2 и тип 3)
	
		// проверка на пустышку
		if(!count($url)) return array();
	
		/**
		 * Т.к. есть ограничения на количество одновременно выполняемых запросов,
		 * будем выполнять запросы последовательно, в порядке очереди.
		 * Как только один запрос отработал, на его место добавляем другой.
		 * Очередь обработки осуществляется на базе $url, каждый добавленный в очередь запрос
		 * убирается из $url.
		*/
	
		if(is_array(current($url))) {
			$type=3;
		} else {
			$type=2;
			$url = array_unique($url);
			$urlKeys = array_flip($url); // исходный массив ключей, нужен для восстановление исходной последовательности
		}
	
		$urlCount = count($url);
	
		// инициализируем "контейнер" для отдельных соединений (мультикурл)
		$cmh = curl_multi_init();
	
		/**
		 * Активные задания для мультикурла. Ключи соответствуют ключам исходного массива url.
		 * Каждое задание состоит из массива элементов с ключами:
		 * 'url' - текущий элемент из массива url
		 * 'options' - опции, с которыми выполняются запросы
		 * 'ch' - curl handle ресурса
		*/
		$tasks = array();
		$result = array(); // массив итоговых результатов
	
		// загрузим первую порцию данных (не более $countTasks $url'ов)
		foreach($url as $currKey=>$curr) {
			if(is_array($curr)) {
				// Считаем что ведем  работу с тип3)
				// в качестве Url передан массив URL'ов с параметрами запроса!!!
				$type = 3;
				//Опции с которыми запрос  будет выполнен.
				//Из опциий выбираются те, которые есть в $curr и дополняются теми которые есть только в $opt
				// array_merge не подходит так как для числовых ключей не происходит замены.
				$cOpt = $curr;
				foreach(array_diff_key($opt, $curr) as $k=>$v){
					$cOpt[$k] = $v;
				}
				// инициализируем отдельное соединение (поток)
				$tasks[$currKey] = array(
						'url' => $curr,
						'options' => $cOpt,
						'ch' => self::getCurl($cOpt),
				);
			} else {
				//Считаем что ведем  работу с тип2)
				//в качестве Url передан индексированный массив URL'ов
				$type = 2;
				// инициализируем отдельное соединение (поток)
				$opt[CURLOPT_URL] = $curr;
				$tasks[$currKey] = array(
						'url' => $curr,
						'options' => $opt,
						'ch' => self::getCurl($opt),
				);
			}
	
			// добавляем дескриптор потока в мультикурл
			curl_multi_add_handle($cmh, $tasks[$currKey]['ch']);
	
			// удаляем из очереди
			unset($url[$currKey]);
	
			// можем одновременно выполнять не более $countTasks задач!
			if(count($tasks) >= $countTasks) break;
		}
	
		// количество активных потоков
		$active = null;
	
		// запускаем выполнение потоков
		do {
			$mrc = curl_multi_exec($cmh, $active);
		}
		while ($mrc == CURLM_CALL_MULTI_PERFORM);
	
		// выполняем, пока не обработаем все URL'ы
		//по мере завершения  потоков, не ожидая завершения всех добавляем новые потоки
		while (count($tasks)) {
			// если есть ещё активные потоки, то повторный пуск
			if($active) {
				$mrc = curl_multi_exec($cmh, $active);
			}
	
			if(($info = curl_multi_info_read($cmh)) !== false) {
				// здесь должен завершиться хотя бы один поток
	
				// сюда попадаем если результат с ошибкой
				if($info['result'] != CURLM_OK) {
					$ch = $info['handle'];
					// ищем ключ для $url по дескриптору потока в массиве заданий
					foreach ($tasks as $u=>$task) {
						if($task['ch'] == $ch) break;
					}
	
					if(isset($tasks[$u]['options']['logfile']) && is_resource($tasks[$u]['options']['logfile'])) {
						$req = isset($tasks[$u]['options']['content']) ? "Request: {$tasks[$u]['options']['content']}\n" : "";
						fwrite($tasks[$u]['options']['logfile'], date("d.m.Y H:i:s") . " Url: {$tasks[$u]['url']['url']}\n {$req} Ошибка CURL!!!!: \n".print_r($info, true)."\n curl_getinfo:\n".print_r(curl_getinfo($ch), true)."\n curl_error: \n" . print_r(curl_error($ch), true) . "\n");
					} else {
						// ОШИБКА при логировании, оставляем на потом
					}
				}
	
				// если поток завершился
				if ($info['msg'] == CURLMSG_DONE) {
					$ch = $info['handle'];
					// ищем ключ для $url по дескриптору потока в массиве заданий
					foreach ($tasks as $u=>$task) {
						if($task['ch'] == $ch) break;
					}
					// забираем содержимое
					$content = curl_multi_getcontent($ch);
	
					// Отсекание по mime фильтру
					if(isset($tasks[$u]['options']['mimefilter']) && is_array($tasks[$u]['options']['mimefilter']) && count($tasks[$u]['options']['mimefilter']) > 0) {
						// В дальнейшем эту  обработку переставим внутрь классов обработчиков
						$mime = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
						$clean_content=true;
						foreach($tasks[$u]['options']['mimefilter'] as $m){
							if(mb_stripos($mime,$m) !==false )
								$clean_content=false;
							break;
						}
						if($clean_content) $content='';
					}
	
					if(isset($tasks[$u]['options']['callback']) && is_array($tasks[$u]['options']['callback']) && count($tasks[$u]['options']['callback']) >= 2) {
						// если передана callback-функция, вызываем её для обработки результата
						// пред последний параметр результат выполнения CURL
						// последний параметр сам CURL
						$callback = array($tasks[$u]['options']['callback'][0], $tasks[$u]['options']['callback'][1]);
						array_splice($tasks[$u]['options']['callback'], 0, 2); // если функция принимает параметры по ссылке, то их и подавать нужно по ссылке
	
						$result[$u] = call_user_func_array(
								$callback,
								array_merge(array($u, (string)$content, $info['result'], $ch), $tasks[$u]['options']['callback'])
						);
					} else {
						// или просто возвращаем контент для текущего Url
						if($info['result'] == CURLM_OK)
							$result[$u] = (string)$content;
						else
							$result[$u] = null;
					}
	
					// callback-обработчик на прерывание существующих потоков
					// должен возвращать массив ключей потоков, которые нужно завершить не дожидаясь результата
					if(isset($tasks[$u]['options']['callback_abort']) && is_array($tasks[$u]['options']['callback_abort']) && count($tasks[$u]['options']['callback_abort']) >= 2) {
						$callback = array($tasks[$u]['options']['callback_abort'][0], $tasks[$u]['options']['callback_abort'][1]);
						array_splice($tasks[$u]['options']['callback_abort'], 0, 2); // если функция принимает параметры по ссылке, то их и подавать нужно по ссылке
						$callbackAbortResult = call_user_func_array(
								$callback,
								array_merge(array(array_merge(array_diff(array_keys($tasks), array($u)), array_keys($url)), $u, $result[$u], $info['result'], $ch), $tasks[$u]['options']['callback_abort'])
						);
						if(is_array($callbackAbortResult) && count($callbackAbortResult)) {
							foreach ($callbackAbortResult as $key) {
								// отменяем уже выполняющиеся задания
								if(isset($tasks[$key]) && $key != $u) {
									// удаляем поток из мультикурла (тем самым прерывая задачу)
									curl_multi_remove_handle($cmh, $tasks[$key]['ch']);
									// закрываем отдельное соединение (поток)
									curl_close($tasks[$key]['ch']);
									// удаляем задачу из массива задач
									unset($tasks[$key]);
									// добавляем результат
									$result[$key] = null;
								}
								// отменяем задания в очереди на выполнение
								if(isset($url[$key])) {
									unset($url[$key]);
									// добавляем результат
									$result[$key] = null;
								}
							}
						}
					}
					//$tasks[$u]['options']['callback_cache'];
	
					// добавим контент в кэш, если необходимо, cache_delete может быть получен из callbakc функции
					if(isset($tasks[$u]['options']['cached']) && $tasks[$u]['options']['cached'] == true && !isset($result[$u]['cache_delete'] ) ) {
						HttpCache::putToCache(HttpCache::getHash($tasks[$u]['options']), $content);
					}
	
					// пишем лог если надо
					if(isset($tasks[$u]['options']['logfile']) && is_resource($tasks[$u]['options']['logfile']) && curl_errno($ch) > 0) {
						fwrite($logFile, curl_error($ch)."\n\n");
					}
					// очистка памяти
					curl_multi_remove_handle($cmh, $ch); // удаляем поток из мультикурла
					curl_close($ch); // закрываем отдельное соединение (поток)
					unset($tasks[$u]); // удаляем задачу из массива задач
	
					// если есть ещё необработанные $url, добавляем один из них
					if(count($url)) {
						reset($url);
						$curr = current($url);
						$currKey = key($url);
						if(is_array($curr)) {
							// тип 3
							//Опции с которыми запрос  будет выполнен.
							//Из опциий выбираются те, которые есть в $curr и дополняются теми которые есть только в $opt
							// array_merge не подходит так как для числовых ключей не происходит замены.
							//$cOpt=array_intersect_key($curr, $opt);
							$cOpt = $curr;
							foreach(array_diff_key($opt,$curr) as $k=>$v){
								$cOpt[$k]=$v;
							}
							$tasks[$currKey] = array(
									'url' => $curr,
									'options' => $cOpt,
									'ch' => self::getCurl($cOpt),
							);
						} else {
							// тип 2
							// инициализируем отдельное соединение (поток)
							$opt[CURLOPT_URL] = $curr;
							//$tasks[$url[$urlKeys[$i]]] = $ch;
							//$tasks[$urlKeys[$i]] = $ch;
							$tasks[$currKey] = array(
									'url' => $curr,
									'options' => $opt,
									'ch' => self::getCurl($opt),
							);
						}
	
						// добавляем дескриптор потока в мультикурл
						curl_multi_add_handle($cmh, $tasks[$currKey]['ch']);
	
						// отправляем на выполнение
						$mrc = curl_multi_exec($cmh, $active);
	
						// удаляем из очереди
						unset($url[$currKey]);
					}
				}
			}
		}
	
		// закрываем мультикурл
		curl_multi_close($cmh);
		//Произвожу пересортировку
		if($type == 2) {
			// от тип два ожидается ответ в котором ключи массива это URL поданные на вход и расположенные в исходном порядке.
			// array_combine здесь не применим, т.к. не ориентируется на ключи, а делает все в порядке следования через итератор
			// тем самым путая данные
	
			$new_result = array();
			foreach ($urlKeys as $u=>$key) {
				$new_result[$u] = $result[$key];
			}
			$result = $new_result;
		}
		return $result;
	}
	
	/**
	 * Возвращает  проинициализированный  опциями полностью готовый  к работе  объект cUrl
	 * URL для обработки принудительно преобразовывается idn_to_ascii()
	 * @param array $opt Набор опций для  инициализации cUrl
	 * @return cUrl
	 */
	public static function getCurl($opt) {
		$ch = curl_init();
		//в 99% случаев этот метод используется для др методв, все данные к этому моменту должны быть проверены
		if (!is_array($opt) || count($opt)==0 )
			return $ch;
	
		//Собор и обработка  ключей, для подачи в curl_setopt_array
		$curl_opt=array();
		//подготовка  дефолтных значений некоторых ключей на  curl
		$curl_opt[CURLOPT_HEADER] = 0; // включать ли заголовки в ответ
		$curl_opt[CURLOPT_RETURNTRANSFER] = 1; // возвращаем значение в качестве строки
		$curl_opt[CURLOPT_SSL_VERIFYPEER] = 0;
	
		$curl_opt[CURLOPT_TIMEOUT] = 90;
		$curl_opt[CURLOPT_ENCODING] = 'gzip,deflate'; // сжатие данных теперь идёт по умолчанию
	
		foreach($opt as $key=>$val) {
			$cu_key=$key;
	
			//Для ключа cookie_path, отдельная  проверка, тк ему сответствуют два ключа опций
			if($cu_key=='cookie_path'){
				$curl_opt[CURLOPT_COOKIEFILE]=$val;
				$curl_opt[CURLOPT_COOKIEJAR] =$val;
				continue;
			}
			if($cu_key=='ssl_verificate'){
				$curl_opt[CURLOPT_SSL_VERIFYPEER] = $val;
				continue;
			}
			if(($cu_key=='ip' || $cu_key==CURLOPT_INTERFACE) && empty($val)){
				continue;
			}
			//обработка коротких значений, список  значений  описан в  self::getContent()
			if(!is_numeric($key)){
				switch ($key) {
					case 'url':            $cu_key=CURLOPT_URL;       break;
					case 'proxy':          $cu_key=CURLOPT_PROXY;     break;
					case 'ip':             $cu_key=CURLOPT_INTERFACE; break;
					case 'content':        $cu_key=CURLOPT_POSTFIELDS;break;
					case 'user_agent':     $cu_key=CURLOPT_USERAGENT; break;
					case 'timeout':        $cu_key=CURLOPT_TIMEOUT;   break;
					case 'headers':        $cu_key=CURLOPT_HTTPHEADER;   break;
					case 'followlocation': $cu_key=CURLOPT_FOLLOWLOCATION; break;
					case 'mimefilter':     $cu_key='';   continue 2; break;
					case 'callback':       $cu_key='';   continue 2; break;
					case 'logfile':        $cu_key='';   continue 2; break;
					default: continue 2; break;
				}
			}
	
			//Удостоверяемся в правильности написания url
			if($cu_key==CURLOPT_URL) {
				/*if(!empty($val) && ($d= MiscHelper::getUrlDomain2($val)) !== ($du = idn_to_ascii($d)) )
					$val = str_ireplace($d,$du,$val);*/
	
				if(mb_strripos($val, 'http://')===false && mb_strripos($val, 'https://')===false)
					$val = "http://".$val;
			}
			//добавление ключа для curl
			$curl_opt[$cu_key]=$val;
			// если есть ключ, указывающий идти через прокси, добавим CURLOPT_COOKIESESSION,
			// чтобы каждый запрос создавалась новая сессия
			if($cu_key == CURLOPT_PROXY) {
				$curl_opt[CURLOPT_COOKIESESSION] = 1;
				// ХАРДКОД ДЛЯ TOR-PROXY
				if($val == '127.0.0.1:9050') $curl_opt[CURLOPT_PROXYTYPE] = CURLPROXY_SOCKS5;
			}
		}
		//Указываем всем  подключениям  использовать ipv4
		$curl_opt[CURLOPT_IPRESOLVE]=CURL_IPRESOLVE_V4;
	
		curl_setopt_array($ch, $curl_opt);
	
		return $ch;
	}
	
	/**
	 * Получает домен из URL.
	 * Url может быть валидным, не валидным, punycode
	 * @param string|array $urlList URL или массив URL'ов
	 * @param bool $withURI Добавлять или нет URI
	 * @param bool $toLowerCase Преобразовывать результат в нижний регистр
	 * @return array
	 */
	public static function getURLDomain2($urlsList, $withURI = false, $toLowerCase = false){
		$pattern = '%^
		(?#Protocol)
		(?:(?:ht|f)tp(?:s?)\:\/\/|~\/|\/)?
		(?#Username:Password)
		(?:\w+:\w+@)?
		(?#Subdomains)
		(?P<SubDomain>
		(?:(?:[-\p{L}\p{Nd}_]+\.)+
		(?#TopLevel Domains)
		(?P<TopLevel>(?:com|org|net|gov|pro|mil|biz|info|mobi|name|aero|jobs|museum|travel|cat|[-\p{L}\p{Nd}]{8}|[\p{L}\p{Nd}]{2}))
		))
		(?P<Uri>.*)%smxuSX';
	
		$urls = is_array($urlsList) ? $urlsList : array($urlsList);
	
		foreach ($urls as $k=>$url) {
			preg_match_all($pattern,  $url, $m );
			if(!isset($m['SubDomain'][0])){
				$urls[$k] = MiscHelper::getURLDomain($url, $withURI);
			}else{
				$urls[$k] = $m['SubDomain'][0];
				if($withURI && isset($m['Uri'][0])) $urls[$k] .= $m['Uri'][0];
			}
			if ( stripos($urls[$k], 'www.') === 0 ){
				$urls[$k] = mb_substr($urls[$k],4);
			}
			if($toLowerCase) $urls[$k] = mb_strtolower($urls[$k]);
		}
	
		return is_array($urlsList) ? $urls : $urls[0];
	}
}